Введение
В эпоху цифровых технологий надёжность пароля стала одним из ключевых факторов безопасности личных данных и онлайн-аккаунтов. Однако создавать действительно устойчивые к взлому пароли непросто: многие пользователи выбирают простые и легко угадываемые комбинации, что существенно снижает уровень защиты.
В этой статье мы подробно разберём современный генератор паролей с гибкими настройками, который позволяет создавать надёжные и уникальные пароли с учётом пожеланий пользователя. Особенностью сервиса является интеграция библиотеки zxcvbn – инструмента для оценки и проверки стойкости любого произвольного пароля. Она позволяет получить объективную оценку безопасности и рекомендует способы усиления пароля.
Используемые технологии и библиотеки
1. HTML5 и CSS3
HTML5 – это язык разметки веб-страниц, позволяющий структурировать контент и задавать семантику. В коде используется семантическая разметка, теги и атрибуты, обеспечивающие правильную организацию формы генератора паролей.
CSS3 – это язык стилей, который отвечает за внешний вид страницы. Здесь применены современные возможности, такие как CSS-переменные, адаптивная верстка (медиа-запросы), псевдоэлементы и переходы, что позволяет реализовать светлую и тёмную тему и удобный интерфейс.
2. JavaScript (ES6+)
Язык программирования, встроенный в браузеры, используется для динамического управления страницей. В коде применяется современный синтаксис ES6+ (например, const, let, стрелочные функции, работа с массивами и объектами) для реализации логики генерации пароля, обработки событий интерфейса, валидации и сохранения настроек.
3. Web Crypto API (crypto.getRandomValues())
Это встроенный в браузеры API для безопасной и криптографически стойкой генерации случайных чисел. Он играет критическую роль в генерации паролей, обеспечивая высокий уровень случайности и безопасности, значительно превосходящий обычный Math.random().
4. LocalStorage
API браузера для хранения данных локально на стороне клиента. Используется для сохранения пользовательских настроек (выбранной длины пароля, включённых групп символов, темы и прочего), чтобы при повторном посещении сайта эти настройки автоматом применялись, улучшая пользовательский опыт.
5. Библиотека zxcvbn (версия 4.4.2)
Это открытая JavaScript-библиотека от Dropbox для оценки надёжности паролей. zxcvbn анализирует пароль по множеству параметров, включая частотность слов, шаблоны повторений, последовательности и словарные слова, и выдаёт оценку устойчивости (от 0 до 4), энтропию, предупреждения и рекомендации. В проекте используется для объективной проверки паролей.
Возможности программы
- Генерация паролей с регулируемой длиной
- От 12 до 64 символов, выбирается через выпадающий список.
- Выбор типов символов для пароля:
- Цифры (0–9)
- Заглавные буквы (A–Z)
- Строчные буквы (a–z)
- Шестнадцатеричные символы (0–9, A–F)
- Исключение похожих символов (i, l, 1, L, o, 0, O)
- Включение специальных символов (набор по умолчанию "!@#$%^&*")
- Ввод дополнительных пользовательских спецсимволов (максимум 30 печатных ASCII символов)
- Автоматическое управление доступностью опций:
- При включении шестнадцатеричных символов остальные группы символов блокируются (кроме исключения похожих).
- Генерация пароля с гарантиями безопасности:
- Использование криптографически стойкой функции
crypto.getRandomValuesдля случайности. - Обеспечение присутствия в пароле символов из каждой выбранной группы.
- Вставка 1–2 специальных символов случайно, если включена опция спецсимволов.
- Удаление более двух подряд идущих одинаковых символов для читаемости.
- Дополнительное перемешивание итогового пароля.
- Оценка надёжности сгенерированного или произвольного пароля:
- Подключена библиотека zxcvbn.
- Отображается битовая энтропия и уровень (Очень слабый / Слабый / Средний / Хороший / Отличный).
- Показ предупреждений и рекомендаций на русском языке с локализацией.
- Проверка произвольного пароля:
- Ввод пароля пользователя в отдельном поле.
- Проверка с выводом оценки и рекомендаций.
- Максимальная длина проверяемого пароля – 128 символов.
- Дружелюбный пользовательский интерфейс:
- Плавный переключатель светлой и тёмной темы интерфейса с запоминанием выбора.
- Стилизация элементов формы и кнопок.
- Интерактивные переключатели (custom switches).
- Индикация доступности кнопок (включена/выключена).
- Уведомления пользователю:
- Всплывающие toast-сообщения, например, при копировании пароля.
- Функция копирования сгенерированного пароля в буфер обмена:
- Кнопка копирования активна только при наличии корректного пароля в результате.
- Сохранение и загрузка настроек:
- Использование localStorage для сохранения выбранных опций и темы между сессиями.
- Доступность:
- Использование aria-атрибутов для поддержки экранных читалок и адаптивное обновление информации (aria-live).
- Управление фокусом и понятные подсказки.
- «Это очень распространённый пароль»
- «Прямые ряды клавиш, например qwerty»
- «Добавьте ещё одно-два слова, лучше редко встречающиеся.»
- «Избегайте повторяющихся слов и символов.»
Обзор Кода
Генерация паролей с использованием криптографически стойких случайных чисел
- Основной источник случайности – встроенный браузерный API
crypto.getRandomValues. Он заполняет массивUint32Arrayкриптостойкими случайными значениями. - Функция
getCharFromSet(set)выбирает из переданного набора символов случайный индекс, используяcrypto.getRandomValues, гарантируя качество случайности. - При генерации пароля:
- Формируется общий набор символов (
charset), учитывая выбранные пользователем группы: цифры, заглавные, строчные, шестнадцатеричные, спецсимволы, исключение похожих символов. - Для каждого символа в пароле берётся случайный индекс из
randomValuesс учётом длины набора символов. - Если
crypto.getRandomValuesне поддерживается (крайний случай), используетсяMath.random()как fallback. - Дополнительно вставляются 1-2 спецсимвола (если включены) в случайные позиции для увеличения сложности.
- Гарантируется присутствие хотя бы одного символа из каждой выбранной группы, для предотвращения однородности.
- Итоговый массив символов
passwordCharsперемешивается (shuffleArray) также используя криптостойкую случайность. - Удаляются повторяющиеся подряд символы больше 2 для удобочитаемости.
- Итоговый пароль отображается на странице.
- Используется адаптивная вёрстка с CSS-переменными для светлой и тёмной темы.
- Все элементы управления снабжены aria-атрибутами для доступности, например,
aria-liveв поле результата для динамического чтения экранными читалками. - Кнопки и переключатели имеют понятные подписи и теги
<label>. - Переключатели реализованы в виде кастомных стилизованных слайдеров (
switch), удобных для клика и переключения. - Контролируется состояние кнопок: кнопка генерации блокируется, если не выбрана ни одна категория символов.
- Результат генерации можно выделить и скопировать, кнопка копирования активируется только при наличии корректного результата.
- Есть поле для проверки произвольного пароля со своей кнопкой и динамической обратной связью.
- Используется всплывающее уведомление (toast) при копировании, чтобы пользователь видел подтверждение.
- Интегрирована библиотека zxcvbn для оценки сложности пароля.
- Функция
generatePassword()иcheckPasswordStrength()вызывают zxcvbn, получают оценку и энтропию. - Результат оценки выводится с цветовой маркировкой и текстовыми баллами от «Очень слабый» до «Отличный».
- Предупреждения и рекомендации локализованы на русский язык через словари
translations.warningиtranslations.suggestions. - Рекомендации выводятся списком под результатом, поясняя, как улучшить пароль (например, избегать повторов и последовательностей).
- Все динамические тексты проходят через функцию
escapeHTML()для защиты от XSS. - Выпадающий список для выбора длины пароля от 12 до 64 символов автоматически создаётся при загрузке страницы.
- Переключатели для:
- Цифр (0–9)
- Заглавных букв (A–Z)
- Строчных букв (a–z)
- Шестнадцатеричных символов (0–9, A–F)
- Исключение похожих символов (
il1Lo0O) - Включение специальных символов (набор из
!@#$%^&*) - Имеется поле для ввода дополнительно выбранных спецсимволов (до 30), куда пользователи могут добавить свои символы.
- При выборе шестнадцатеричных символов автоматически отключаются остальные группы, чтобы избежать конфликта.
- Настройки валидируются, кнопка генерации блокируется, если пользователь не включил ни одну группу символов.
- При выборе опции исключения похожих символов из итогового набора удаляются все символы из перечня:
i l 1 L o 0 O. - Для специальных символов задан базовый набор и добавляется пользовательский набор из поля ввода.
- Специальные символы вставляются в пароль отдельным шагом, чтобы гарантировать их присутствие и повысить прочность.
- Все настройки (длина, выбранные группы символов, пользовательские спецсимволы, тема) сохраняются в
localStorageпод ключомpasswordGeneratorSettings. - При загрузке страницы эти настройки восстанавливаются и применяются к элементам интерфейса.
- Переключатель темы добавляет или убирает класс
darkу тегаbody, что меняет цвета по CSS-переменным. - Атрибуты
aria-checkedу переключателя темы синхронизированы с текущим состоянием. - Изменения в настройках сразу сохраняются и доступны при следующем посещении.
Оценка надёжности пароля
Оценка надёжности пароля в этом коде происходит с помощью библиотеки zxcvbn.js – мощного инструмента для анализа сложности пароля с учётом вероятных шаблонов и слабых мест.
Вот подробный разбор, как именно работает оценка:
- Ввод пароля для проверки и начальная валидация
- Если поле пустое – выводится просьба ввести пароль.
- Если длина пароля превышает 128 символов – выводится ошибка о максимальной длине.
- Запуск оценки пароля с помощью zxcvbn
entropy– оценка энтропии (битовой стойкости) пароля: чем выше, тем сложнее подобрать.score– числовая оценка от 0 (очень слабый) до 4 (отличный), характеризующая надёжность.feedback– объект с предупреждениями и рекомендациями для пользователя.- Формирование вывода оценки
- Если
scoreот 3 и выше – показывается текст "Высокая надёжность". - Если
entropyравен 0, ноscore> 0 – показывается "10+ бит". - Иначе показывается конкретное значение энтропии в битах, напр.
45 бит. - Предупреждения и рекомендации
- «Это очень распространённый пароль»
- «Прямые ряды клавиш, например qwerty»
- «Добавьте ещё одно-два слова, лучше редко встречающиеся.»
- «Избегайте повторяющихся слов и символов.»
- Защита от XSS
- Интерактивность и отзывы
- Библиотека zxcvbn оценивает даже сложные шаблоны, например последовательности, повторения, предсказуемые слова, даты, и даёт объективную меру сложности – энтропию в битах.
- Оценка
scoreот 0 до 4 – простой красочный индикатор уровня. - Предупреждения и рекомендации помогают пользователю сделать пароль сильнее.
Цифры от 0 до 4 в оценке надёжности пароля (поле score из zxcvbn) означают уровни стойкости пароля:
- 0 – Очень слабый
- 1 – Слабый
- 2 – Средний
- 3 – Хороший
- 4 – Отличный
Полный код генератора
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Генератор паролей с спецсимволами по условию</title>
<style>
:root {
--color-bg-light: #f0f4f8;
--color-text-light: #222;
--color-card-light: #fff;
--color-primary-light: #4a90e2;
--color-primary-hover-light: #357abd;
--color-toggle-bg-light: #ccc;
--color-bg-dark: #121212;
--color-text-dark: #eee;
--color-card-dark: #1e1e1e;
--color-primary-dark: #4a90e2;
--color-primary-hover-dark: #357abd;
--color-toggle-bg-dark: #555;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 1rem;
background-color: var(--color-bg-light);
color: var(--color-text-light);
transition: background-color 0.3s ease, color 0.3s ease;
display: flex;
justify-content: center;
}
.container {
background-color: var(--color-card-light);
padding: 2rem 1.5rem;
border-radius: 12px;
max-width: 460px;
width: 100%;
box-sizing: border-box;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
h1 {
text-align: center;
margin-bottom: 1.5rem;
font-weight: 700;
font-size: 1.8rem;
}
label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
}
select, input[type="text"] {
width: 100%;
padding: 0.55rem 0.75rem;
border-radius: 8px;
border: 1.8px solid #bbb;
font-size: 1rem;
font-weight: 500;
box-sizing: border-box;
transition: border-color 0.3s ease;
color: var(--color-text-light);
background-color: var(--color-card-light);
}
select:focus, input[type="text"]:focus {
outline: none;
border-color: var(--color-primary-light);
}
.toggle-group {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1rem 0 0.7rem;
}
.toggle-label {
font-size: 1rem;
font-weight: 500;
}
.switch {
position: relative;
display: inline-block;
width: 48px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: var(--color-toggle-bg-light);
border-radius: 26px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.slider:before {
content: "";
position: absolute;
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: transform 0.3s ease;
}
input:checked + .slider {
background-color: var(--color-primary-light);
}
input:checked + .slider:before {
transform: translateX(22px);
}
button {
margin-top: 1.8rem;
width: 100%;
padding: 14px 0;
font-size: 1.15rem;
font-weight: 700;
color: white;
border: none;
background-color: var(--color-primary-light);
border-radius: 12px;
cursor: pointer;
box-shadow: 0 4px 14px rgba(74,144,226,0.65);
transition: background-color 0.3s ease;
}
button:hover:not(:disabled) {
background-color: var(--color-primary-hover-light);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#result {
margin-top: 1.6rem;
padding: 15px 12px;
background: #e1ecf9;
border-radius: 12px;
word-break: break-word;
font-weight: 700;
font-size: 1.3rem;
user-select: all;
color: #1c2a4a;
transition: background-color 0.3s ease, color 0.3s ease;
min-height: 1.6em;
}
#entropy {
margin-top: 0.8rem;
font-size: 1rem;
font-weight: 600;
color: #333;
text-align: center;
}
#checkPassword, #checkBtn {
margin-top: 1rem;
}
#checkPassword {
padding: 0.55rem 0.75rem;
font-size: 1rem;
border-radius: 8px;
border: 1.8px solid #bbb;
box-sizing: border-box;
width: 100%;
color: var(--color-text-light);
background-color: var(--color-card-light);
transition: border-color 0.3s ease;
}
#checkPassword:focus {
outline: none;
border-color: var(--color-primary-light);
}
#checkResult {
margin-top: 0.6rem;
font-weight: 600;
font-size: 1rem;
text-align: center;
opacity: 1;
transition: opacity 0.3s ease;
min-height: 1.2em;
}
#checkSuggestions {
margin-top: 0.3rem;
font-size: 0.9rem;
color: #555;
line-height: 1.3;
padding: 0 10px;
text-align: left;
}
#copyBtn {
margin-top: 0.8rem;
background-color: #6a9be3;
border-radius: 10px;
padding: 10px;
font-weight: 600;
font-size: 1rem;
border: none;
cursor: pointer;
color: white;
box-shadow: 0 2px 10px rgba(74,144,226,0.7);
transition: background-color 0.3s ease;
width: 100%;
}
#copyBtn:hover {
background-color: #547fca;
}
#symbolsHint {
font-size: 0.85rem;
color: #666;
margin-top: 0.2rem;
user-select: none;
}
body.dark {
background-color: var(--color-bg-dark);
color: var(--color-text-dark);
}
body.dark .container {
background-color: var(--color-card-dark);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8);
}
body.dark select, body.dark input[type="text"] {
border-color: #555;
background-color: var(--color-card-dark);
color: var(--color-text-dark);
}
body.dark select:focus, body.dark input[type="text"]:focus {
border-color: var(--color-primary-dark);
}
body.dark .slider {
background-color: var(--color-toggle-bg-dark);
}
body.dark input:checked + .slider {
background-color: var(--color-primary-dark);
}
body.dark #result {
background: #2b3a67;
color: #bcdfff;
}
body.dark #entropy {
color: #b0c6f6;
}
body.dark button {
background-color: var(--color-primary-dark);
box-shadow: 0 4px 14px rgba(74,144,226,0.85);
}
body.dark button:hover:not(:disabled) {
background-color: #2e5ea6;
}
body.dark #copyBtn {
background-color: #547fca;
box-shadow: 0 2px 10px rgba(74,144,226,0.85);
}
body.dark #copyBtn:hover {
background-color: #4367a4;
}
.theme-switch-wrapper {
margin-bottom: 1.3rem;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-weight: 600;
font-size: 1rem;
user-select: none;
}
.theme-switch-wrapper .switch {
width: 52px;
height: 28px;
}
.theme-label {
cursor: pointer;
}
@media (max-width: 480px) {
.container {
padding: 1.6rem 1.2rem;
border-radius: 10px;
max-width: 100%;
}
h1 {
font-size: 1.5rem;
}
button {
font-size: 1.05rem;
}
#result {
font-size: 1.15rem;
}
#copyBtn {
font-size: 1rem;
padding: 9px;
}
}
.hidden {
opacity: 0;
}
</style>
</head>
<body>
<div class="container" role="main" aria-label="Генератор паролей">
<div class="theme-switch-wrapper">
<label class="theme-label" for="theme-switch">Темная тема</label>
<label class="switch">
<input type="checkbox" id="theme-switch" aria-checked="false" role="switch" />
<span class="slider"></span>
</label>
</div>
<h1>Генератор паролей</h1>
<label for="length">Количество символов:</label>
<select id="length" name="length" aria-describedby="length-desc"></select>
<div id="length-desc" style="font-size:0.85rem; color:#666; margin-bottom:0.7rem;">Минимум 12, максимум 64 символа</div>
<div class="toggle-group">
<span class="toggle-label">0-9</span>
<label class="switch" title="Включить цифры" aria-label="Включить цифры">
<input type="checkbox" id="numbers" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">A-Z</span>
<label class="switch" title="Включить заглавные буквы" aria-label="Включить заглавные буквы">
<input type="checkbox" id="uppercase" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">a-z</span>
<label class="switch" title="Включить строчные буквы" aria-label="Включить строчные буквы">
<input type="checkbox" id="lowercase" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-group" id="hex-toggle-wrapper">
<span class="toggle-label">Шестнадцатеричные (0-9, A-F)</span>
<label class="switch" title="Включить шестнадцатеричные символы" aria-label="Включить шестнадцатеричные символы">
<input type="checkbox" id="hexadecimal" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">Исключить похожие символы (i l 1 L o 0 O)</span>
<label class="switch" title="Исключить похожие символы" aria-label="Исключить похожие символы">
<input type="checkbox" id="excludeSimilar" />
<span class="slider"></span>
</label>
</div>
<div class="toggle-group">
<span class="toggle-label">Включить специальные символы</span>
<label class="switch" title="Включить специальные символы" aria-label="Включить специальные символы">
<input type="checkbox" id="enableSymbols" />
<span class="slider"></span>
</label>
</div>
<div style="margin-top:-0.7rem; margin-bottom:1rem; font-size:0.85rem; color:#666; user-select:none;">
Используются символы: !@#$%^&*
</div>
<div>
<label for="symbols">Дополнительные спецсимволы (макс. 30 символов):</label>
<input type="text" id="symbols" placeholder="Можно ввести свои символы" autocomplete="off" spellcheck="false" maxlength="30" aria-describedby="symbolsHint" />
<div id="symbolsHint" style="font-size:0.85rem; color:#666; margin-top:0.2rem; user-select:none;">
Можно вводить любые печатные ASCII символы, например: !@#$%^&*()-_=+[]{}|;:,.<>?/\\
</div>
</div>
<button id="generateBtn" disabled aria-disabled="true">Сгенерировать</button>
<p id="result" title="Результат генерации пароля" aria-live="polite" aria-atomic="true"></p>
<button id="copyBtn" disabled aria-disabled="true">Скопировать пароль</button>
<p id="entropy" aria-live="polite" aria-atomic="true"></p>
<hr style="margin: 2rem 0" />
<label for="checkPassword">Проверить пароль на надёжность:</label>
<input
type="text" id="checkPassword" placeholder="Введи или вставь пароль" autocomplete="off" spellcheck="false"
maxlength="128" aria-describedby="checkPasswordDesc" />
<div id="checkPasswordDesc" style="font-size:0.85rem; color:#666; margin-bottom:0.3rem;">
Максимальная длина пароля для проверки: 128 символов
</div>
<button id="checkBtn">Проверить</button>
<p id="checkResult" aria-live="polite" aria-atomic="true"></p>
<div id="checkSuggestions"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const lengthSelect = document.getElementById('length');
const numbersCheckbox = document.getElementById('numbers');
const uppercaseCheckbox = document.getElementById('uppercase');
const lowercaseCheckbox = document.getElementById('lowercase');
const hexadecimalCheckbox = document.getElementById('hexadecimal');
const excludeSimilarCheckbox = document.getElementById('excludeSimilar');
const enableSymbolsCheckbox = document.getElementById('enableSymbols');
const symbolsInput = document.getElementById('symbols');
const resultEl = document.getElementById('result');
const entropyEl = document.getElementById('entropy');
const copyBtn = document.getElementById('copyBtn');
const generateBtn = document.getElementById('generateBtn');
const themeSwitch = document.getElementById('theme-switch');
const hexToggleWrapper = document.getElementById('hex-toggle-wrapper');
const checkPasswordInput = document.getElementById('checkPassword');
const checkBtn = document.getElementById('checkBtn');
const checkResult = document.getElementById('checkResult');
const checkSuggestions = document.getElementById('checkSuggestions');
// Заполнить длину пароля с 12 по 64
if(lengthSelect.options.length === 0) {
for(let i=12; i<=64; i++) {
lengthSelect.insertAdjacentHTML('beforeend', `<option value="${i}">${i}</option>`);
}
lengthSelect.value = 12;
}
const STORAGE_KEY = 'passwordGeneratorSettings';
const translations = {
warning: {
"This is a top-10 common password": "Это один из 10 наиболее распространённых паролей",
"This is a very common password": "Это очень распространённый пароль",
"This is similar to a commonly used password": "Это похоже на часто используемый пароль",
"Straight rows of keys like qwerty": "Прямые ряды клавиш, например qwerty",
"Short keyboard patterns are easy to guess": "Короткие клавиатурные паттерны легко угадать",
"Repeats like abcabcabc": "Повторы символов, например abcabcabc",
"Sequence of characters": "Последовательность символов",
"Contains recent year": "Содержит актуальный год",
"Contains dates or years": "Содержит даты или годы"
},
suggestions: {
"Add another word or two. Uncommon words are better.": "Добавьте ещё одно-два слова, лучше редко встречающиеся.",
"Avoid repeated words and characters.": "Избегайте повторяющихся слов и символов.",
"Avoid sequences like abc or 123.": "Избегайте последовательностей вроде abc или 123.",
"Avoid recent years.": "Избегайте актуальных годов.",
"Use a longer keyboard pattern with more variation.": "Используйте более длинный паттерн с большей вариативностью."
}
};
function translateWarning(warning) {
return translations.warning[warning] || warning;
}
function translateSuggestion(suggestion) {
return translations.suggestions[suggestion] || suggestion;
}
function applyTheme(dark) {
if(dark) {
document.body.classList.add('dark');
themeSwitch.checked = true;
themeSwitch.setAttribute('aria-checked', 'true');
} else {
document.body.classList.remove('dark');
themeSwitch.checked = false;
themeSwitch.setAttribute('aria-checked', 'false');
}
}
function loadSettings() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if(!saved) return;
const settings = JSON.parse(saved);
if(typeof settings.darkTheme === 'boolean') {
applyTheme(settings.darkTheme);
}
if(typeof settings.length === 'number' && settings.length >= 12) lengthSelect.value = settings.length;
if(typeof settings.numbers === 'boolean') numbersCheckbox.checked = settings.numbers;
if(typeof settings.uppercase === 'boolean') uppercaseCheckbox.checked = settings.uppercase;
if(typeof settings.lowercase === 'boolean') lowercaseCheckbox.checked = settings.lowercase;
if(typeof settings.hexadecimal === 'boolean') hexadecimalCheckbox.checked = settings.hexadecimal;
if(typeof settings.excludeSimilar === 'boolean') excludeSimilarCheckbox.checked = settings.excludeSimilar;
if(typeof settings.enableSymbols === 'boolean') enableSymbolsCheckbox.checked = settings.enableSymbols;
if(typeof settings.symbols === 'string') symbolsInput.value = settings.symbols;
} catch {}
}
function saveSettings() {
const settings = {
darkTheme: themeSwitch.checked,
length: parseInt(lengthSelect.value),
numbers: numbersCheckbox.checked,
uppercase: uppercaseCheckbox.checked,
lowercase: lowercaseCheckbox.checked,
hexadecimal: hexadecimalCheckbox.checked,
excludeSimilar: excludeSimilarCheckbox.checked,
enableSymbols: enableSymbolsCheckbox.checked,
symbols: symbolsInput.value
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
// Валидирует минимальный набор символов включён, отключает кнопку, если нет.
function validateOptions() {
// Шестнадцатеричные блокируют остальные группы кроме excludeSimilar
if(hexadecimalCheckbox.checked) {
numbersCheckbox.disabled = true;
uppercaseCheckbox.disabled = true;
lowercaseCheckbox.disabled = true;
} else {
numbersCheckbox.disabled = false;
uppercaseCheckbox.disabled = false;
lowercaseCheckbox.disabled = false;
}
// Проверяем выбранные группы (исключая excludeSimilar, symbols, custom символы)
const groupsSelected = hexadecimalCheckbox.checked || numbersCheckbox.checked || uppercaseCheckbox.checked || lowercaseCheckbox.checked;
generateBtn.disabled = !groupsSelected;
generateBtn.setAttribute('aria-disabled', String(!groupsSelected));
// Кнопка копирования доступна только если есть результат
copyBtn.disabled = !resultEl.textContent || resultEl.textContent.length === 0;
copyBtn.setAttribute('aria-disabled', copyBtn.disabled.toString());
}
symbolsInput.addEventListener('input', () => {
// Фильтруем по печатным ASCII и максимум 30 символов
const safeChars = /^[\u0021-\u007e]*$/;
let filtered = [...symbolsInput.value].filter(ch => safeChars.test(ch));
filtered = [...new Set(filtered)];
if(filtered.length > 30) filtered = filtered.slice(0,30);
const filteredStr = filtered.join('');
if(filteredStr !== symbolsInput.value) symbolsInput.value = filteredStr;
saveSettings();
});
[lengthSelect, numbersCheckbox, uppercaseCheckbox, lowercaseCheckbox,
hexadecimalCheckbox, excludeSimilarCheckbox, enableSymbolsCheckbox, symbolsInput].forEach(elem => {
elem.addEventListener('change', () => {
saveSettings();
validateOptions();
});
});
themeSwitch.addEventListener('change', () => {
applyTheme(themeSwitch.checked);
saveSettings();
});
validateOptions();
function getCharFromSet(set) {
try {
const idx = crypto.getRandomValues(new Uint32Array(1))[0] % set.length;
return set.charAt(idx);
} catch {
// fallback (маловероятно)
return set.charAt(Math.floor(Math.random() * set.length));
}
}
function shuffleArray(arr) {
try {
for(let i = arr.length - 1; i > 0; i--) {
const j = crypto.getRandomValues(new Uint32Array(1))[0] % (i + 1);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
} catch {
// fallback - простой shuffle
for(let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}
function getScoreColor(score) {
switch(score) {
case 0: return 'red';
case 1: return 'orangered';
case 2: return 'orange';
case 3: return 'green';
case 4: return 'darkgreen';
default: return 'black';
}
}
function getScoreLabel(score) {
switch(score) {
case 0: return 'Очень слабый';
case 1: return 'Слабый';
case 2: return 'Средний';
case 3: return 'Хороший';
case 4: return 'Отличный';
default: return 'Неопределён';
}
}
function removeConsecutiveRepeats(str, maxRepeats = 2) {
if (!str) return str;
let result = str[0];
let count = 1;
for (let i = 1; i < str.length; i++) {
if (str[i] === str[i - 1]) {
count++;
if (count <= maxRepeats) {
result += str[i];
}
} else {
count = 1;
result += str[i];
}
}
return result;
}
function showToast(message) {
let toast = document.createElement('div');
toast.textContent = message;
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.left = '50%';
toast.style.transform = 'translateX(-50%)';
toast.style.backgroundColor = 'rgba(50,50,50,0.85)';
toast.style.color = 'white';
toast.style.padding = '10px 20px';
toast.style.borderRadius = '8px';
toast.style.fontWeight = '600';
toast.style.zIndex = '1000';
toast.style.userSelect = 'none';
toast.style.fontSize = '1rem';
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity 0.5s';
toast.style.opacity = '0';
}, 1500);
setTimeout(() => document.body.removeChild(toast), 2000);
}
function generatePassword() {
const length = parseInt(lengthSelect.value);
if(length < 12 || length > 64) {
resultEl.textContent = 'Длина пароля должна быть от 12 до 64 символов';
entropyEl.textContent = '';
return;
}
const includeNumbers = numbersCheckbox.checked;
const includeUppercase = uppercaseCheckbox.checked;
const includeLowercase = lowercaseCheckbox.checked;
const includeHex = hexadecimalCheckbox.checked;
const excludeSimilar = excludeSimilarCheckbox.checked;
const includeSymbols = enableSymbolsCheckbox.checked;
let customSymbols = symbolsInput.value;
customSymbols = [...new Set(customSymbols)].join('');
const sets = {
numbers: '0123456789',
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
lowercase: 'abcdefghijklmnopqrstuvwxyz',
hex: '0123456789ABCDEF',
symbols: '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\',
};
const specialSet = '!@#$%^&*';
let charset = '';
if (includeHex) {
charset = sets.hex;
} else {
if (includeNumbers) charset += sets.numbers;
if (includeUppercase) charset += sets.uppercase;
if (includeLowercase) charset += sets.lowercase;
}
if (customSymbols.length) charset += customSymbols;
if (excludeSimilar) {
charset = charset.split('').filter(ch => !'il1Lo0O'.includes(ch)).join('');
}
if (charset.length === 0) {
resultEl.textContent = 'Выберите хотя бы одну группу символов!';
entropyEl.textContent = '';
return;
}
let passwordChars = [];
let randomValues;
try {
randomValues = new Uint32Array(length);
crypto.getRandomValues(randomValues);
} catch {
// fallback
randomValues = new Uint32Array(length);
for(let i=0; i<length; i++) randomValues[i] = Math.floor(Math.random() * 0xFFFFFFFF);
}
for (let i = 0; i < length; i++) {
passwordChars.push(charset[randomValues[i] % charset.length]);
}
if (includeSymbols) {
const maxSpecial = Math.min(2, length);
let countSpecial = 1;
try {
countSpecial = 1 + (crypto.getRandomValues(new Uint8Array(1))[0] % 2);
} catch {
countSpecial = 1 + (Math.floor(Math.random()*2));
}
countSpecial = Math.min(countSpecial, maxSpecial);
for (let i = 0; i < countSpecial; i++) {
let pos;
try {
pos = crypto.getRandomValues(new Uint32Array(1))[0] % length;
} catch {
pos = Math.floor(Math.random() * length);
}
const symIdx = crypto.getRandomValues ? crypto.getRandomValues(new Uint32Array(1))[0] % specialSet.length : Math.floor(Math.random() * specialSet.length);
passwordChars[pos] = specialSet[symIdx];
}
}
let requiredChars = [];
if (includeHex) {
requiredChars.push(getCharFromSet(sets.hex));
} else {
if (includeNumbers) requiredChars.push(getCharFromSet(sets.numbers));
if (includeUppercase) requiredChars.push(getCharFromSet(sets.uppercase));
if (includeLowercase) requiredChars.push(getCharFromSet(sets.lowercase));
}
if (customSymbols.length) requiredChars.push(getCharFromSet(customSymbols));
requiredChars.forEach(ch => {
let pos, attempts = 0;
do {
try {
pos = crypto.getRandomValues(new Uint32Array(1))[0] % length;
} catch {
pos = Math.floor(Math.random() * length);
}
attempts++;
} while (includeSymbols && specialSet.includes(passwordChars[pos]) && attempts < 10);
passwordChars[pos] = ch;
});
shuffleArray(passwordChars);
let password = passwordChars.join('');
password = removeConsecutiveRepeats(password, 2);
while(password.length < length) {
const idx = crypto.getRandomValues ? crypto.getRandomValues(new Uint32Array(1))[0] % charset.length : Math.floor(Math.random()*charset.length);
password += charset[idx];
}
password = password.split('').sort(() => 0.5 - Math.random()).join('');
resultEl.textContent = password;
let evalResult = null;
try {
evalResult = zxcvbn(password);
} catch {}
const entropy = (evalResult && typeof evalResult.entropy === 'number') ? Math.round(evalResult.entropy) : 0;
const score = (evalResult && typeof evalResult.score === 'number') ? evalResult.score : 0;
let entropyDisplay;
if (score >= 3) {
entropyDisplay = 'Высокая надёжность';
} else if (entropy <= 0 && score > 0) {
entropyDisplay = '10+ бит';
} else {
entropyDisplay = entropy + ' бит';
}
entropyEl.innerHTML = `Оценка надежности: <b>${entropyDisplay}</b><br>
Уровень: <b style="color:${getScoreColor(score)}">${getScoreLabel(score)}</b>`;
validateOptions();
}
function escapeHTML(str) {
return str.replace(/[&<>"']/g, function(m) {
return {'&':'&', '<':'<', '>':'>', '"':'"', '\'':'''}[m];
});
}
function checkPasswordStrength() {
const pwd = checkPasswordInput.value.trim();
if(!pwd) {
checkResult.textContent = "Введите пароль для проверки.";
checkResult.style.color = "black";
checkSuggestions.textContent = '';
return;
}
if(pwd.length > 128) {
checkResult.textContent = "Пароль слишком длинный (максимум 128 символов).";
checkResult.style.color = "red";
checkSuggestions.textContent = '';
return;
}
let evalResult = null;
try {
evalResult = zxcvbn(pwd);
} catch {}
const entropyCheck = (evalResult && typeof evalResult.entropy === 'number') ? Math.round(evalResult.entropy) : 0;
const scoreCheck = (evalResult && typeof evalResult.score === 'number') ? evalResult.score : 0;
let entropyDisplayCheck;
if (scoreCheck >= 3) {
entropyDisplayCheck = 'Высокая надёжность';
} else if (entropyCheck <= 0 && scoreCheck > 0) {
entropyDisplayCheck = '10+ бит';
} else {
entropyDisplayCheck = entropyCheck + ' бит';
}
checkResult.innerHTML = `Оценка надежности: <b>${entropyDisplayCheck}</b><br>
Уровень: <b style="color:${getScoreColor(scoreCheck)}">${getScoreLabel(scoreCheck)}</b>`;
if (evalResult && (evalResult.feedback.warning || (evalResult.feedback.suggestions && evalResult.feedback.suggestions.length > 0))) {
checkSuggestions.innerHTML = '';
if(evalResult.feedback.warning) {
checkSuggestions.innerHTML += `<div>\u26a0\ufe0f <b>Предупреждение:</b> ${escapeHTML(translateWarning(evalResult.feedback.warning))}</div>`;
}
if (evalResult.feedback.suggestions && evalResult.feedback.suggestions.length > 0) {
checkSuggestions.innerHTML += `<div>\u1f4a1 <b>Рекомендации:</b><ul>${evalResult.feedback.suggestions.map(s=>`<li>${escapeHTML(translateSuggestion(s))}</li>`).join('')}</ul></div>`;
}
} else {
checkSuggestions.textContent = '';
}
}
generateBtn.addEventListener('click', () => {
generateBtn.disabled = true;
generateBtn.setAttribute('aria-disabled', 'true');
setTimeout(() => {
generatePassword();
generateBtn.disabled = false;
generateBtn.setAttribute('aria-disabled', 'false');
}, 50);
});
copyBtn.addEventListener('click', () => {
const pwd = resultEl.textContent;
if(!pwd || pwd === 'Выберите хотя бы одну группу символов!' || pwd === 'Длина пароля должна быть от 12 до 64 символов') return;
navigator.clipboard.writeText(pwd).then(() => showToast('Пароль скопирован в буфер обмена!'));
});
checkBtn.addEventListener('click', () => {
checkResult.classList.add('hidden');
setTimeout(() => {
checkPasswordStrength();
checkResult.classList.remove('hidden');
}, 200);
});
checkPasswordInput.addEventListener('input', () => {
if (checkPasswordInput.value.trim() === '') {
checkResult.textContent = '';
checkSuggestions.textContent = '';
return;
}
if(checkPasswordInput.value.length > 128){
checkResult.textContent = "Пароль слишком длинный (максимум 128 символов).";
checkResult.style.color = "red";
checkSuggestions.textContent = '';
return;
}
checkPasswordStrength();
});
loadSettings();
validateOptions();
});
</script>
</body>
</html>